<?php
/*
 *  $Id: Builder.php 2939 2007-10-19 14:23:42Z Jonathan.Wage $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

/**
 * Doctrine_Migration_Builder
 *
 * @package     Doctrine
 * @subpackage  Migration
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author      Jonathan H. Wage <jwage@mac.com>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.doctrine-project.org
 * @since       1.0
 * @version     $Revision: 2939 $
 */
class Doctrine_Migration_Builder extends Doctrine_Builder
{
	/**
	 * The path to your migration classes directory
	 *
	 * @var string
	 */
	private $migrationsPath = '';

	/**
	 * File suffix to use when writing class definitions
	 *
	 * @var string $suffix
	 */
	private $suffix = '.php';

	/**
	 * Instance of the migration class for the migration classes directory
	 *
	 * @var Doctrine_Migration $migration
	 */
	private $migration;

	/**
	 * Class template used for writing classes
	 *
	 * @var $tpl
	 */
	private static $tpl;

	/**
	 * Instantiate new instance of the Doctrine_Migration_Builder class
	 *
	 * <code>
	 * $builder = new Doctrine_Migration_Builder('/path/to/migrations');
	 * </code>
	 *
	 * @return void
	 */
	public function __construct($migrationsPath = null)
	{
		if ($migrationsPath instanceof Doctrine_Migration) {
			$this->setMigrationsPath($migrationsPath->getMigrationClassesDirectory());
			$this->migration = $migrationsPath;
		} else if (is_dir($migrationsPath)) {
			$this->setMigrationsPath($migrationsPath);
			$this->migration = new Doctrine_Migration($migrationsPath);
		}

		$this->loadTemplate();
	}

	/**
	 * Set the path to write the generated migration classes
	 *
	 * @param string path   the path where migration classes are stored and being generated
	 * @return void
	 */
	public function setMigrationsPath($path)
	{
		Doctrine_Lib::makeDirectories($path);

		$this->migrationsPath = $path;
	}

	/**
	 * Get the path where generated migration classes are written to
	 *
	 * @return string       the path where migration classes are stored and being generated
	 */
	public function getMigrationsPath()
	{
		return $this->migrationsPath;
	}

	/**
	 * Loads the class template used for generating classes
	 *
	 * @return void
	 */
	protected function loadTemplate()
	{
		if (isset(self::$tpl)) {
			return;
		}

		self::$tpl =<<<END
/**
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class %s extends %s
{
    public function up()
    {
%s
    }

    public function down()
    {
%s
    }
}
END;
	}

	/**
	 * Generate migrations from a Doctrine_Migration_Diff instance
	 *
	 * @param  Doctrine_Migration_Diff $diff Instance to generate changes from
	 * @return array $changes  Array of changes produced from the diff
	 */
	public function generateMigrationsFromDiff(Doctrine_Migration_Diff $diff)
	{
		$changes = $diff->generateChanges();

		$up = array();
		$down = array();

		if ( ! empty($changes['dropped_tables'])) {
			foreach ($changes['dropped_tables'] as $tableName => $table) {
				$up[] = $this->buildDropTable($table);
				$down[] = $this->buildCreateTable($table);
			}
		}

		if ( ! empty($changes['created_tables'])) {
			foreach ($changes['created_tables'] as $tableName => $table) {
				$up[] = $this->buildCreateTable($table);
				$down[] = $this->buildDropTable($table);
			}
		}

		if ( ! empty($changes['dropped_columns'])) {
			foreach ($changes['dropped_columns'] as $tableName => $removedColumns) {
				foreach ($removedColumns as $name => $column) {
					$up[] = $this->buildRemoveColumn($tableName, $name, $column);
					$down[] = $this->buildAddColumn($tableName, $name, $column);
				}
			}
		}

		if ( ! empty($changes['created_columns'])) {
			foreach ($changes['created_columns'] as $tableName => $addedColumns) {
				foreach ($addedColumns as $name => $column) {
					$up[] = $this->buildAddColumn($tableName, $name, $column);
					$down[] = $this->buildRemoveColumn($tableName, $name, $column);
				}
			}
		}

		if ( ! empty($changes['changed_columns'])) {
			foreach ($changes['changed_columns'] as $tableName => $changedColumns) {
				foreach ($changedColumns as $name => $column) {
					$up[] = $this->buildChangeColumn($tableName, $name, $column);
				}
			}
		}

		if ( ! empty($up) || ! empty($down)) {
			$up = implode("\n", $up);
			$down = implode("\n", $down);
			$className = 'Version' . $this->migration->getNextMigrationClassVersion();
			$this->generateMigrationClass($className, array(), $up, $down);
		}

		$up = array();
		$down = array();
		if ( ! empty($changes['dropped_foreign_keys'])) {
			foreach ($changes['dropped_foreign_keys'] as $tableName => $droppedFks) {
				if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) {
					continue;
				}

				foreach ($droppedFks as $name => $foreignKey) {
					$up[] = $this->buildDropForeignKey($tableName, $foreignKey);
					$down[] = $this->buildCreateForeignKey($tableName, $foreignKey);
				}
			}
		}

		if ( ! empty($changes['dropped_indexes'])) {
			foreach ($changes['dropped_indexes'] as $tableName => $removedIndexes) {
				if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) {
					continue;
				}

				foreach ($removedIndexes as $name => $index) {
					$up[] = $this->buildRemoveIndex($tableName, $name, $index);
					$down[] = $this->buildAddIndex($tableName, $name, $index);
				}
			}
		}

		if ( ! empty($changes['created_foreign_keys'])) {
			foreach ($changes['created_foreign_keys'] as $tableName => $createdFks) {
				if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) {
					continue;
				}

				foreach ($createdFks as $name => $foreignKey) {
					$up[] = $this->buildCreateForeignKey($tableName, $foreignKey);
					$down[] = $this->buildDropForeignKey($tableName, $foreignKey);
				}
			}
		}

		if ( ! empty($changes['created_indexes'])) {
			foreach ($changes['created_indexes'] as $tableName => $addedIndexes) {
				if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) {
					continue;
				}

				foreach ($addedIndexes as $name => $index) {
					if (isset($changes['created_tables'][$tableName]['options']['indexes'][$name])) {
						continue;
					}
					$up[] = $this->buildAddIndex($tableName, $name, $index);
					$down[] = $this->buildRemoveIndex($tableName, $name, $index);
				}
			}
		}

		if ( ! empty($up) || ! empty($down)) {
			$up = implode("\n", $up);
			$down = implode("\n", $down);
			$className = 'Version' . $this->migration->getNextMigrationClassVersion();
			$this->generateMigrationClass($className, array(), $up, $down);
		}
		return $changes;
	}

	/**
	 * Generate a set of migration classes from the existing databases
	 *
	 * @return void
	 */
	public function generateMigrationsFromDb()
	{
		$directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'tmp_doctrine_models';

		Doctrine_Core::generateModelsFromDb($directory);

		$result = $this->generateMigrationsFromModels($directory, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

		Doctrine_Lib::removeDirectories($directory);

		return $result;
	}

	/**
	 * Generate a set of migrations from a set of models
	 *
	 * @param  string $modelsPath    Path to models
	 * @param  string $modelLoading  What type of model loading to use when loading the models
	 * @return boolean
	 */
	public function generateMigrationsFromModels($modelsPath = null, $modelLoading = null)
	{
		if ($modelsPath !== null) {
			$models = Doctrine_Core::filterInvalidModels(Doctrine_Core::loadModels($modelsPath, $modelLoading));
		} else {
			$models = Doctrine_Core::getLoadedModels();
		}

		$models = Doctrine_Core::initializeModels($models);

		$foreignKeys = array();

		foreach ($models as $model) {
			$table = Doctrine_Core::getTable($model);
			if ($table->getTableName() !== $this->migration->getTableName()) {
				$export = $table->getExportableFormat();

				$foreignKeys[$export['tableName']] = $export['options']['foreignKeys'];

				$up = $this->buildCreateTable($export);
				$down = $this->buildDropTable($export);

				$className = 'Add' . Doctrine_Inflector::classify($export['tableName']);

				$this->generateMigrationClass($className, array(), $up, $down);
			}
		}

		if ( ! empty($foreignKeys)) {
			$className = 'AddFks';

			$up = array();
			$down = array();
			foreach ($foreignKeys as $tableName => $definitions)    {
				$tableForeignKeyNames[$tableName] = array();

				foreach ($definitions as $definition) {
					$up[] = $this->buildCreateForeignKey($tableName, $definition);
					$down[] = $this->buildDropForeignKey($tableName, $definition);
				}
			}

			$up = implode("\n", $up);
			$down = implode("\n", $down);
			if ($up || $down) {
				$this->generateMigrationClass($className, array(), $up, $down);
			}
		}

		return true;
	}

	/**
	 * Build the code for creating foreign keys
	 *
	 * @param  string $tableName
	 * @param  array  $definition
	 * @return string $code
	 */
	public function buildCreateForeignKey($tableName, $definition)
	{
		return "        \$this->createForeignKey('" . $tableName . "', '" . $definition['name'] . "', " . $this->varExport($definition, true) . ");";
	}

	/**
	 * Build the code for dropping foreign keys
	 *
	 * @param  string $tableName
	 * @param  array  $definition
	 * @return string $code
	 */
	public function buildDropForeignKey($tableName, $definition)
	{
		return "        \$this->dropForeignKey('" . $tableName . "', '" . $definition['name'] . "');";
	}

	/**
	 * Build the code for creating tables
	 *
	 * @param  string $tableData
	 * @return string $code
	 */
	public function buildCreateTable($tableData)
	{
		$code  = "        \$this->createTable('" . $tableData['tableName'] . "', ";

		$code .= $this->varExport($tableData['columns'], true) . ", ";

		$optionsWeNeed = array('type', 'indexes', 'primary', 'collate', 'charset');

		$options = array();
		foreach ($optionsWeNeed as $option) {
			if (isset($tableData['options'][$option])) {
				$options[$option] = $tableData['options'][$option];
			}
		}

		$code .= $this->varExport($options, true);

		$code .= ");";

		return $code;
	}

	/**
	 * Build the code for dropping tables
	 *
	 * @param  string $tableData
	 * @return string $code
	 */
	public function buildDropTable($tableData)
	{
		return "        \$this->dropTable('" . $tableData['tableName'] . "');";
	}

	/**
	 * Build the code for adding columns
	 *
	 * @param string $tableName
	 * @param string $columnName
	 * @param string $column
	 * @return string $code
	 */
	public function buildAddColumn($tableName, $columnName, $column)
	{
		$length = $column['length'];
		$type = $column['type'];
		unset($column['length'], $column['type']);
		return "        \$this->addColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");";
	}

	/**
	 * Build the code for removing columns
	 *
	 * @param string $tableName
	 * @param string $columnName
	 * @param string $column
	 * @return string $code
	 */
	public function buildRemoveColumn($tableName, $columnName, $column)
	{
		return "        \$this->removeColumn('" . $tableName . "', '" . $columnName. "');";
	}

	/**
	 * Build the code for changing columns
	 *
	 * @param string $tableName
	 * @param string $columnName
	 * @param string $column
	 * @return string $code
	 */
	public function buildChangeColumn($tableName, $columnName, $column)
	{
		$length = $column['length'];
		$type = $column['type'];
		unset($column['length'], $column['type']);
		return "        \$this->changeColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");";
	}

	/**
	 * Build the code for adding indexes
	 *
	 * @param string $tableName
	 * @param string $indexName
	 * @param string $index
	 * @return sgtring $code
	 */
	public function buildAddIndex($tableName, $indexName, $index)
	{
		return "        \$this->addIndex('$tableName', '$indexName', " . $this->varExport($index) . ");";
	}

	/**
	 * Build the code for removing indexes
	 *
	 * @param string $tableName
	 * @param string $indexName
	 * @param string $index
	 * @return string $code
	 */
	public function buildRemoveIndex($tableName, $indexName, $index)
	{
		return "        \$this->removeIndex('$tableName', '$indexName', " . $this->varExport($index) . ");";
	}

	/**
	 * Generate a migration class
	 *
	 * @param string  $className   Class name to generate
	 * @param array   $options     Options for the migration class
	 * @param string  $up          The code for the up function
	 * @param string  $down        The code for the down function
	 * @param boolean $return      Whether or not to return the code.
	 *                             If true return and false it writes the class to disk.
	 * @return mixed
	 */
	public function generateMigrationClass($className, $options = array(), $up = null, $down = null, $return = false)
	{
		$className = Doctrine_Inflector::urlize($className);
		$className = str_replace('-', '_', $className);
		$className = Doctrine_Inflector::classify($className);

		if ($return || ! $this->getMigrationsPath()) {
			return $this->buildMigrationClass($className, null, $options, $up, $down);
		} else {
			if ( ! $this->getMigrationsPath()) {
				throw new Doctrine_Migration_Exception('You must specify the path to your migrations.');
			}

			$next = time() + $this->migration->getNextMigrationClassVersion();
			$fileName = $next . '_' . Doctrine_Inflector::tableize($className) . $this->suffix;

			$class = $this->buildMigrationClass($className, $fileName, $options, $up, $down);

			$path = $this->getMigrationsPath() . DIRECTORY_SEPARATOR . $fileName;
			if (class_exists($className) || file_exists($path)) {
				$this->migration->loadMigrationClass($className);
				return false;
			}

			file_put_contents($path, $class);
			require_once($path);
			$this->migration->loadMigrationClass($className);

			return true;
		}
	}

	/**
	 * Build the code for a migration class
	 *
	 * @param string  $className   Class name to generate
	 * @param string  $fileName    File name to write the class to
	 * @param array   $options     Options for the migration class
	 * @param string  $up          The code for the up function
	 * @param string  $down        The code for the down function
	 * @return string $content     The code for the generated class
	 */
	public function buildMigrationClass($className, $fileName = null, $options = array(), $up = null, $down = null)
	{
		$extends = isset($options['extends']) ? $options['extends']:'Doctrine_Migration_Base';

		$content  = '<?php' . PHP_EOL;

		$content .= sprintf(self::$tpl, $className,
		$extends,
		$up,
		$down);

		return $content;
	}
}